﻿using System;
using System.Runtime.InteropServices;
using SCG = System.Collections.Generic;

using Nemerle.Assertions;
using Nemerle.Utility;

using GASS.OpenCL;
using GASS.OpenCL.OpenCLDriver;

namespace Nemerle.OpenCL {

    /// <summary>
    /// Description of CommandQueue.
    /// </summary>
    [InfoProperty(CLCommandQueueInfo.ReferenceCount, int, Flags(no_cache))]
    [EqualityByIdPattern]
    public sealed class CommandQueue : IDisposable {

        internal this([NotNull] context : Context, [NotNull] info : ContextCreateInfo) {
            this.handle = CommandQueueHandle(chk_err_t clCreateCommandQueue(context.Id, info.Device.Id, info.Flags));
            this.context = context;
            this.device = info.Device;
            this.out_of_order_exec_mode_enable = info.Flags %&& CLCommandQueueProperties.OutOfOrderExecModeEnable;
            this.profiling_enable = info.Flags %&& CLCommandQueueProperties.ProfilingEnable;
        }

        [Accessor] context : Context;

        [Accessor] device : Device;

        handle : CommandQueueHandle;

        public Id : CLCommandQueue {
            get { handle.EntityId }
        }

        mutable out_of_order_exec_mode_enable : bool;

        public OutOfOrderExecModeEnable : bool {
            get { out_of_order_exec_mode_enable }
            set {
                mutable old_value;
                CheckError(clSetCommandQueueProperty(Id, CLCommandQueueProperties.OutOfOrderExecModeEnable, Utils.Converter.Convert(value), ref old_value));
                out_of_order_exec_mode_enable = value;
            }
        }

        mutable profiling_enable : bool;

        public ProfilingEnable : bool {
            get { profiling_enable }
            set {
                mutable old_value;
                CheckError(clSetCommandQueueProperty(Id, CLCommandQueueProperties.ProfilingEnable, Utils.Converter.Convert(value), ref old_value));
                profiling_enable = value;
            }
        }

        public Flush() : void {
            CheckError(clFlush(this.Id));
        }

        public Finish() : void {
            CheckError(clFinish(this.Id));
        }

        public InsertMarker() : Event {
            mutable event_id;
            CheckError(clEnqueueMarker(this.Id, ref event_id));
            Event(this, event_id)
        }

        public InsertBarrier() : void {
            CheckError(clEnqueueBarrier(this.Id));
        }

        public WaitAll(params events : array[Event]) : void {
            def (events_len, events) = Event.ConvertEventList(events);
            when(events_len > 0) 
                CheckError(clEnqueueWaitForEvents(this.Id, events_len, events));
        }

        public WaitAll(events : SCG.IEnumerable[Event]) : void {
            when(null != events)
                WaitAll(SCG.List(events).ToArray());
        }

        #region Task

        public Execute(kernel : Kernel) : void {
            using(evt = BeginExecute(kernel))
                evt.WaitOne();
        }

        public Execute(kernel : Kernel, global_work_size : NDRange, local_work_size : NDRange) : void {
            using(evt = BeginExecute(kernel, global_work_size, local_work_size))
                evt.WaitOne();
        }

        public BeginExecute(kernel : Kernel, params deps : array[Event]) : Event {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            mutable event_id;
            CheckError(clEnqueueTask(Id, kernel.Id, deps_len, deps, ref event_id));
            Event(this, event_id)
        }

        public BeginExecute(kernel : Kernel, global_work_size : NDRange, local_work_size : NDRange, params deps : array[Event]) : Event 
            requires (0 < global_work_size.Length) && (global_work_size.Length <= 3)
            requires (null == local_work_size) || (global_work_size.Length == local_work_size.Length)
        {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            mutable event_id;
            CheckError(clEnqueueNDRangeKernel(Id, kernel.Id, global_work_size.Length :> uint, null, global_work_size, local_work_size, deps_len, deps, ref event_id));
            Event(this, event_id)
        }

        #endregion

        #region Read buffer

        public ReadBuffer(source : Nemerle.OpenCL.Buffer, offset : long, count : long, dest : Array) : void {
            fixed(dest_ptr = dest)
                ReadBuffer(source, offset, count, dest_ptr);
        }

        public ReadBuffer(source : Nemerle.OpenCL.Buffer, offset : long, count : long, dest : IntPtr) : void {
            mutable event_id;
            CheckError(clEnqueueReadBuffer(Id, source.Id, CLBool.True, offset, count, dest, 0, null, ref event_id));
            _ = clReleaseEvent(event_id);
        }

        public BeginReadBuffer(source : Nemerle.OpenCL.Buffer, offset : long, count : long, dest : Array, params deps : array[Event]) : Event {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            def ptr = GCHandle.Alloc(dest, GCHandleType.Pinned);
            try {
                mutable event_id;
                CheckError(clEnqueueReadBuffer(Id, source.Id, CLBool.False, offset, count, Marshal.UnsafeAddrOfPinnedArrayElement(dest, 0), deps_len, deps, ref event_id));
                Event(this, event_id, ptr)
            } catch {
                | e => ptr.Free(); throw e;
            }
        }

        public BeginReadBuffer(source : Nemerle.OpenCL.Buffer, offset : long, count : long, dest : IntPtr, params deps : array[Event]) : Event {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            mutable event_id;
            CheckError(clEnqueueReadBuffer(Id, source.Id, CLBool.False, offset, count, dest, deps_len, deps, ref event_id));
            Event(this, event_id)
        }

        #endregion

        #region Write buffer

        public WriteBuffer(dest : Nemerle.OpenCL.Buffer, offset : long, count : long, source : Array) : void {
            fixed(ptr = source)
                WriteBuffer(dest, offset, count, ptr);
        }

        public WriteBuffer(dest : Nemerle.OpenCL.Buffer, offset : long, count : long, source : IntPtr) : void {
            mutable event_id;
            CheckError(clEnqueueWriteBuffer(Id, dest.Id, CLBool.True, offset, count, source, 0, null, ref event_id));
            _ = clReleaseEvent(event_id);
        }

        public BeginWriteBuffer(dest : Nemerle.OpenCL.Buffer, offset : long, count : long, source : Array, params deps : array[Event]) : Event {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            def ptr = GCHandle.Alloc(dest, GCHandleType.Pinned);
            try {
                mutable event_id;
                CheckError(clEnqueueWriteBuffer(Id, dest.Id, CLBool.False, offset, count, Marshal.UnsafeAddrOfPinnedArrayElement(source, 0), deps_len, deps, ref event_id));
                Event(this, event_id, ptr)
            } catch {
                | e => ptr.Free(); throw e;
            }
        }

        public BeginWriteBuffer(dest : Nemerle.OpenCL.Buffer, offset : long, count : long, source : IntPtr, params deps : array[Event]) : Event {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            mutable event_id;
            CheckError(clEnqueueWriteBuffer(Id, dest.Id, CLBool.False, offset, count, source, deps_len, deps, ref event_id));
            Event(this, event_id)
        }

        #endregion

        #region Read image 2d

        public ReadImage(source : Image2D, origin : NDRange, region : NDRange, row_pitch : long, dest : Array) : void {
            fixed(ptr = dest)
                ReadImage(source, origin, region, row_pitch, ptr);
        }

        public ReadImage(source : Image2D, origin : NDRange, region : NDRange, row_pitch : long, dest : IntPtr) : void {
            def origin = ConvertOrigin2D(origin);
            def region = ConvertRegion2D(region);
            mutable event_id;
            CheckError(clEnqueueReadImage(this.Id, source.Id, CLBool.True, origin, region, row_pitch, 0, dest, 0, null, ref event_id));
            _ = clReleaseEvent(event_id);
        }

        public BeginReadImage(source : Image2D, origin : NDRange, region : NDRange, row_pitch : long, dest : Array, params deps : array[Event]) : Event {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            def origin = ConvertOrigin2D(origin);
            def region = ConvertRegion2D(region);
            def ptr = GCHandle.Alloc(dest, GCHandleType.Pinned);
            try {
                mutable event_id;
                CheckError(clEnqueueReadImage(this.Id, source.Id, CLBool.False, origin, region, row_pitch, 0, Marshal.UnsafeAddrOfPinnedArrayElement(dest, 0), deps_len, deps, ref event_id));
                Event(this, event_id, ptr)
            } catch {
                | e => ptr.Free(); throw e;
            }
        }

        public BeginReadImage(source : Image2D, origin : NDRange, region : NDRange, row_pitch : long, dest : IntPtr, params deps : array[Event]) : Event {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            def origin = ConvertOrigin2D(origin);
            def region = ConvertRegion2D(region);
            mutable event_id;
            CheckError(clEnqueueReadImage(this.Id, source.Id, CLBool.False, origin, region, row_pitch, 0, dest, deps_len, deps, ref event_id));
            Event(this, event_id)
        }

        #endregion

        #region Write image 2d

        public WriteImage(dest : Image2D, origin : NDRange, region : NDRange, row_pitch : long, source : Array) : void {
            fixed(ptr = source)
                WriteImage(dest, origin, region, row_pitch, ptr);
        }

        public WriteImage(dest : Image2D, origin : NDRange, region : NDRange, row_pitch : long, source : IntPtr) : void {
            def origin = ConvertOrigin2D(origin);
            def region = ConvertRegion2D(region);
            mutable event_id;
            CheckError(clEnqueueWriteImage(this.Id, dest.Id, CLBool.True, origin, region, row_pitch, 0, source, 0, null, ref event_id));
            _ = clReleaseEvent(event_id);
        }

        public BeginWriteImage(dest : Image2D, origin : NDRange, region : NDRange, row_pitch : long, source : Array, params deps : array[Event]) : Event {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            def origin = ConvertOrigin2D(origin);
            def region = ConvertRegion2D(region);
            def ptr = GCHandle.Alloc(dest, GCHandleType.Pinned);
            try {
                mutable event_id;
                CheckError(clEnqueueWriteImage(this.Id, dest.Id, CLBool.False, origin, region, row_pitch, 0, Marshal.UnsafeAddrOfPinnedArrayElement(source, 0), deps_len, deps, ref event_id));
                Event(this, event_id, ptr)
            } catch {
                | e => ptr.Free(); throw e;
            }
        }

        public BeginWriteImage(dest : Image2D, origin : NDRange, region : NDRange, row_pitch : long, source : IntPtr, params deps : array[Event]) : Event {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            def origin = ConvertOrigin2D(origin);
            def region = ConvertRegion2D(region);
            mutable event_id;
            CheckError(clEnqueueWriteImage(this.Id, dest.Id, CLBool.False, origin, region, row_pitch, 0, source, deps_len, deps, ref event_id));
            Event(this, event_id)
        }

        #endregion

        #region Read image 3d

        public ReadImage(source : Image3D, origin : NDRange, region : NDRange, row_pitch : long, slice_pitch : long, dest : Array) : void {
            fixed(ptr = dest)
                ReadImage(source, origin, region, row_pitch, slice_pitch, ptr);
        }

        public ReadImage(source : Image3D, origin : NDRange, region : NDRange, row_pitch : long, slice_pitch : long, dest : IntPtr) : void {
            mutable event_id;
            CheckError(clEnqueueReadImage(this.Id, source.Id, CLBool.True, origin, region, row_pitch, slice_pitch, dest, 0, null, ref event_id));
            _ = clReleaseEvent(event_id);
        }

        public BeginReadImage(source : Image3D, origin : NDRange, region : NDRange, row_pitch : long, slice_pitch : long, dest : Array, params deps : array[Event]) : Event {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            def ptr = GCHandle.Alloc(dest, GCHandleType.Pinned);
            try {
                mutable event_id;
                CheckError(clEnqueueReadImage(this.Id, source.Id, CLBool.False, origin, region, row_pitch, slice_pitch,  Marshal.UnsafeAddrOfPinnedArrayElement(dest, 0), deps_len, deps, ref event_id));
                Event(this, event_id, ptr)
            } catch {
                | e => ptr.Free(); throw e;
            }
        }

        public BeginReadImage(source : Image3D, origin : NDRange, region : NDRange, row_pitch : long, slice_pitch : long, dest : IntPtr, params deps : array[Event]) : Event {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            mutable event_id;
            CheckError(clEnqueueReadImage(this.Id, source.Id, CLBool.False, origin, region, row_pitch, slice_pitch, dest, deps_len, deps, ref event_id));
            Event(this, event_id)
        }

        #endregion

        #region Write image 3d

        public WriteImage(dest : Image3D, origin : NDRange, region : NDRange, row_pitch : long, slice_pitch : long, source : Array) : void {
            fixed(ptr = source)
                WriteImage(dest, origin, region, row_pitch, slice_pitch, ptr);
        }

        public WriteImage(dest : Image3D, origin : NDRange, region : NDRange, row_pitch : long, slice_pitch : long, source : IntPtr) : void {
            mutable event_id;
            CheckError(clEnqueueWriteImage(this.Id, dest.Id, CLBool.True, origin, region, row_pitch, slice_pitch, source, 0, null, ref event_id));
            _ = clReleaseEvent(event_id);
        }

        public BeginWriteImage(dest : Image3D, origin : NDRange, region : NDRange, row_pitch : long, slice_pitch : long, source : Array, params deps : array[Event]) : Event {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            def ptr = GCHandle.Alloc(source, GCHandleType.Pinned);
            try {
                mutable event_id;
                CheckError(clEnqueueWriteImage(this.Id, dest.Id, CLBool.False, origin, region, row_pitch, slice_pitch,  Marshal.UnsafeAddrOfPinnedArrayElement(source, 0), deps_len, deps, ref event_id));
                Event(this, event_id, ptr)
            } catch {
                | e => ptr.Free(); throw e;
            }
        }

        public BeginWriteImage(dest : Image3D, origin : NDRange, region : NDRange, row_pitch : long, slice_pitch : long, source : IntPtr, params deps : array[Event]) : Event {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            mutable event_id;
            CheckError(clEnqueueWriteImage(this.Id, dest.Id, CLBool.False, origin, region, row_pitch, slice_pitch, source, deps_len, deps, ref event_id));
            Event(this, event_id)
        }

        #endregion

        #region Copy memory

        public Copy(source : Image, source_origin : NDRange, region : NDRange, dest : Image, dest_origin : NDRange) : void {
            mutable convert_region = false;
            def source_origin = CheckOrigin(source, source_origin, ref convert_region);
            def dest_origin = CheckOrigin(dest, dest_origin, ref convert_region);
            def region = if(convert_region) ConvertRegion2D(region) else region;
            mutable event_id;
            CheckError(clEnqueueCopyImage(this.Id, source.Id, dest.Id, source_origin, dest_origin, region, 0, null, ref event_id));
            _ = clReleaseEvent(event_id);
        }

        public BeginCopy(source : Image, source_origin : NDRange, region : NDRange, dest : Image, dest_origin : NDRange, params deps : array[Event]) : Event {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            mutable convert_region = false;
            def source_origin = CheckOrigin(source, source_origin, ref convert_region);
            def dest_origin = CheckOrigin(dest, dest_origin, ref convert_region);
            def region = if(convert_region) ConvertRegion2D(region) else region;
            mutable event_id;
            CheckError(clEnqueueCopyImage(this.Id, source.Id, dest.Id, source_origin, dest_origin, region, deps_len, deps, ref event_id));
            Event(this, event_id)
        }

        public Copy(source : Image, source_origin : NDRange, region : NDRange, dest : Nemerle.OpenCL.Buffer, dest_offset : long) : void {
            mutable convert_region = false;
            def source_origin = CheckOrigin(source, source_origin, ref convert_region);
            def region = if(convert_region) ConvertRegion2D(region) else region;
            mutable event_id;
            CheckError(clEnqueueCopyImageToBuffer(this.Id, source.Id, dest.Id, source_origin, region, dest_offset, 0, null, ref event_id));
            _ = clReleaseEvent(event_id);
        }

        public BeginCopy(source : Image, source_origin : NDRange, region : NDRange, dest : Nemerle.OpenCL.Buffer, dest_offset : long, params deps : array[Event]) : Event {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            mutable convert_region = false;
            def source_origin = CheckOrigin(source, source_origin, ref convert_region);
            def region = if(convert_region) ConvertRegion2D(region) else region;
            mutable event_id;
            CheckError(clEnqueueCopyImageToBuffer(this.Id, source.Id, dest.Id, source_origin, region, dest_offset, deps_len, deps, ref event_id));
            Event(this, event_id)
        }

        public Copy(source : Nemerle.OpenCL.Buffer, source_offset : long, dest : Image, dest_origin : NDRange, region : NDRange) : void {
            mutable convert_region = false;
            def dest_origin = CheckOrigin(dest, dest_origin, ref convert_region);
            def region = if(convert_region) ConvertRegion2D(region) else region;
            mutable event_id;
            CheckError(clEnqueueCopyBufferToImage(this.Id, source.Id, dest.Id, source_offset, dest_origin, region, 0, null, ref event_id));
            _ = clReleaseEvent(event_id);
        }

        public BeginCopy(source : Nemerle.OpenCL.Buffer, source_offset : long, dest : Image, dest_origin : NDRange, region : NDRange, params deps : array[Event]) : Event {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            mutable convert_region = false;
            def dest_origin = CheckOrigin(dest, dest_origin, ref convert_region);
            def region = if(convert_region) ConvertRegion2D(region) else region;
            mutable event_id;
            CheckError(clEnqueueCopyBufferToImage(this.Id, source.Id, dest.Id, source_offset, dest_origin, region, deps_len, deps, ref event_id));
            Event(this, event_id)
        }

        #endregion

        #region Memory mapping

        public MapBuffer(buffer : Nemerle.OpenCL.Buffer, flags : CLMapFlags, offset : long, count : long) : MemoryMap {
            mutable event_id;
            def ptr = CheckErrorTail(clEnqueueMapBuffer(this.Id, buffer.Id, CLBool.True, flags, offset, count, 0, null, ref event_id));
            _ = clReleaseEvent(event_id);
            MemoryMap(this, buffer, ptr, 0, 0)
        }

        public BeginMapBuffer(buffer : Nemerle.OpenCL.Buffer, flags : CLMapFlags, offset : long, count : long, params deps : array[Event]) : MemoryMap {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            mutable event_id;
            def ptr = CheckErrorTail(clEnqueueMapBuffer(this.Id, buffer.Id, CLBool.False, flags, offset, count, deps_len, deps, ref event_id));
            MemoryMap(this, buffer, ptr, 0, 0, Event(this, event_id))
        }

        public MapImage(image : Image2D, flags : CLMapFlags, origin : NDRange, region : NDRange) : MemoryMap {
            def origin = ConvertOrigin2D(origin);
            def region = ConvertRegion2D(region);
            mutable row_pitch;
            mutable slice_pitch;
            mutable event_id;
            def ptr = CheckErrorTail(clEnqueueMapImage(this.Id, image.Id, CLBool.True, flags, origin, region, ref row_pitch, ref slice_pitch, 0, null, ref event_id));
            _ = clReleaseEvent(event_id);
            MemoryMap(this, image, ptr, row_pitch, slice_pitch)
        }

        public BeginMapImage(image : Image2D, flags : CLMapFlags, origin : NDRange, region : NDRange, params deps : array[Event]) : MemoryMap {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            def origin = ConvertOrigin2D(origin);
            def region = ConvertRegion2D(region);
            mutable row_pitch;
            mutable slice_pitch;
            mutable event_id;
            def ptr = CheckErrorTail(clEnqueueMapImage(this.Id, image.Id, CLBool.False, flags, origin, region, ref row_pitch, ref slice_pitch, deps_len, deps, ref event_id));
            MemoryMap(this, image, ptr, row_pitch, slice_pitch, Event(this, event_id))
        }

        public MapImage(image : Image3D, flags : CLMapFlags, origin : NDRange, region : NDRange) : MemoryMap {
            mutable row_pitch;
            mutable slice_pitch;
            mutable event_id;
            def ptr = CheckErrorTail(clEnqueueMapImage(this.Id, image.Id, CLBool.True, flags, origin, region, ref row_pitch, ref slice_pitch, 0, null, ref event_id));
            _ = clReleaseEvent(event_id);
            MemoryMap(this, image, ptr, row_pitch, slice_pitch)
        }

        public BeginMapImage(image : Image3D, flags : CLMapFlags, origin : NDRange, region : NDRange, params deps : array[Event]) : MemoryMap {
            def (deps_len, deps) = Event.ConvertEventList(deps);
            mutable row_pitch;
            mutable slice_pitch;
            mutable event_id;
            def ptr = CheckErrorTail(clEnqueueMapImage(this.Id, image.Id, CLBool.False, flags, origin, region, ref row_pitch, ref slice_pitch, deps_len, deps, ref event_id));
            MemoryMap(this, image, ptr, row_pitch, slice_pitch, Event(this, event_id))
        }

        #endregion

        #region IDisposable

        public Dispose() : void {
            when(null != handle)
                handle.Close();
        }

        #endregion

        #region Utils

        private static CheckOrigin(image : Image, origin : NDRange, need_convert_region : ref bool) : NDRange {
            match(image.Type) {
                | CLMemObjectType.Image2D =>
                    need_convert_region = true;
                    ConvertOrigin2D(origin)
                | _ => origin
            }
        }

        private static ConvertOrigin2D(origin : NDRange) : NDRange {
            array[ origin[0], origin[1], 0 ]
        }

        private static ConvertRegion2D(region : NDRange) : NDRange {
            array[ region[0], region[1], 1 ]
        }

        #endregion

    }

}
